const { promisify } = require('util')
const { Kind } = require('graphql/language')
const { GraphQLScalarType, defaultFieldResolver } = require('graphql')
const {
  SchemaDirectiveVisitor,
  AuthenticationError,
  ApolloError
} = require('apollo-server')
const { DataSource } = require('apollo-datasource')
const jwt = require('jsonwebtoken')

function serialize (value) {
  return value instanceof Date ? value.toISOString() : null
}

function parseValue (value) {
  return value === null ? null : new Date(value)
}

function parseLiteral (ast) {
  return ast.kind === Kind.STRING ? parseValue(ast.value) : null
}

// 判断 context.user 中是否有当前 field 的使用权限
// 没有权限的, 是否可以通过加入 userId 条件允许访问
// 拥有权限的, 还需要通过 group 和 organization 来进行分级处理
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition (field) {
    // console.log(this, field)
    const { resolve = defaultFieldResolver } = field
    // const { resolve } = field
    field.resolve = async function (...args) {
      // args[2] 是 context 参数
      const { user, redis } = args[2]
      // 没有 token 提示 401
      if (!user || !user.id) {
        throw new AuthenticationError('you must be logged in')
      }
      // 从 redis 中获取 user 的详细数据
      const curUser = await redis.getAsync(user.id)
      // 没有 token 提示 401
      if (!curUser) {
        throw new AuthenticationError('you must be logged in')
      }
      // 判断没有权限提示 403
      if (curUser.hasPermission(field.name)) {
        throw new ApolloError(
          'you have not permission to do' + field.name,
          'NOT_PERMISSION'
        )
      }
      const result = await resolve.apply(this, args)
      /* if (typeof result === 'string') {
        return result.toUpperCase()
      } */
      console.log(this, field)
      return result
    }
  }
}

class DataAPI extends DataSource {
  constructor ({ models }) {
    super()
    this.models = models
  }

  /**
   * This is a function that gets called by ApolloServer when being setup.
   * This function gets called with the datasource config including things
   * like caches and context. We'll assign this.context to the request context
   * here, so we can know about the user making requests
   */
  initialize (config) {
    this.context = config.context
  }

  async findById ({ id }) {
    const edges = await this.models.findById(id).exec()
    return edges
  }

  async findOne ({ where }) {
    const edges = await this.models.findOne(where).exec()
    return edges
  }

  async find ({ where, pagination, sort }) {
    const option = { sort }
    if (pagination && pagination.limit) {
      option.limit = pagination.limit
    } else if (pagination && pagination.startCursor) {
      where['_id'] = { $gt: pagination.startCursor }
    } else if (pagination && pagination.endCursor) {
      where['_id'] = { $lt: pagination.endCursor }
    }
    const edges = await this.models.find(where, null, option).exec()
    console.log(where, pagination, sort, edges)
    return edges
  }

  async create ({ data }) {
    const edges = await this.models.create(data)
    return edges
  }

  async findByIdAndUpdate ({ id, data }) {
    const edges = await this.models.findByIdAndUpdate(id, data).exec()
    return edges
  }

  async findOneAndUpdate ({ where, data }) {
    const edges = await this.models.findOneAndUpdate(where, data).exec()
    return edges
  }

  async updateMany ({ where, data }) {
    const edges = await this.models.updateMany(where, data).exec()
    return edges
  }

  async findByIdAndRemove ({ id }) {
    const edges = await this.models.findByIdAndRemove(id).exec()
    return edges
  }

  async findOneAndRemove ({ where }) {
    const edges = await this.models.findOneAndRemove(where).exec()
    return edges
  }

  async deleteMany ({ where }) {
    const edges = await this.models.deleteMany(where).exec()
    return edges
  }
}

const queryResolver = ({ name, api }) => ({
  [name]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].findOne(args)
    return entity
  },
  [`${name}s`]: async (_, args, { dataSources }) => {
    const edges = await dataSources[api].find(args)
    // console.log(edges)
    if (edges && edges.length > 0) {
      return {
        pageInfo: {
          hasNextPage: false,
          hasPreviousPage: false,
          startCursor: edges[0]._id.toString(),
          endCursor: edges[edges.length - 1]._id.toString()
        },
        edges
      }
    }
    return {
      pageInfo: {},
      edges
    }
  }
})

const mutationResolver = ({ name, api }) => ({
  [`create${name}`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].create(args)
    return entity
  },
  [`update${name}ById`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].findByIdAndUpdate(args)
    return entity
  },
  [`update${name}`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].findOneAndUpdate(args)
    return entity
  },
  [`update${name}s`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].updateMany(args)
    return entity
  },
  [`delete${name}`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].findOneAndRemove(args)
    return entity
  },
  [`delete${name}s`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].deleteMany(args)
    return entity
  },
  [`delete${name}ById`]: async (_, args, { dataSources }) => {
    const entity = await dataSources[api].findByIdAndRemove(args)
    return entity
  }
})

module.exports = {
  DataAPI,
  AuthDirective,
  queryResolver,
  mutationResolver,
  Sort: {
    ASC: 1,
    DESC: -1
  },
  ISODate: new GraphQLScalarType({
    name: 'ISODate',
    description: 'JavaScript Date object as an ISO timestamp',
    serialize,
    parseValue,
    parseLiteral
  }),
  redisify: redis => ({
    getAsync: promisify(redis.get).bind(redis)
  }),
  getUser: (token, secret) => {
    if (token) {
      // invalid token - synchronous
      try {
        const id = jwt.verify(token, secret)
        return { id }
      } catch (err) {
        // err
        // 验证失败处理
        console.log(err)
        return null
      }
    }
    return null
  }
}
